package org.bouncycastle.jcajce.provider.asymmetric.ec;

import java.io.ByteArrayOutputStream;
import java.security.AlgorithmParameters;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.spec.AlgorithmParameterSpec;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.CipherSpi;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.ShortBufferException;

import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.KeyEncoder;
import org.bouncycastle.crypto.agreement.ECDHBasicAgreement;
import org.bouncycastle.crypto.digests.SHA1Digest;
import org.bouncycastle.crypto.engines.AESEngine;
import org.bouncycastle.crypto.engines.DESedeEngine;
import org.bouncycastle.crypto.engines.IESEngine;
import org.bouncycastle.crypto.generators.ECKeyPairGenerator;
import org.bouncycastle.crypto.generators.EphemeralKeyPairGenerator;
import org.bouncycastle.crypto.generators.KDF2BytesGenerator;
import org.bouncycastle.crypto.macs.HMac;
import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher;
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECKeyGenerationParameters;
import org.bouncycastle.crypto.params.ECKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.params.IESParameters;
import org.bouncycastle.crypto.params.IESWithCipherParameters;
import org.bouncycastle.crypto.parsers.ECIESPublicKeyParser;
import org.bouncycastle.jcajce.provider.asymmetric.util.IESUtil;
import org.bouncycastle.jce.interfaces.ECKey;
import org.bouncycastle.jce.interfaces.ECPrivateKey;
import org.bouncycastle.jce.interfaces.ECPublicKey;
import org.bouncycastle.jce.interfaces.IESKey;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.spec.IESParameterSpec;
import org.bouncycastle.util.Strings;


public class IESCipher
    extends CipherSpi
{
	private IESEngine engine;
	private int                     state = -1;
	private ByteArrayOutputStream   buffer = new ByteArrayOutputStream();
	private AlgorithmParameters     engineParam = null;  
	private IESParameterSpec        engineSpec = null;
	private AsymmetricKeyParameter	key;
	private SecureRandom			random;
	private boolean                 dhaesMode = false;
    private AsymmetricKeyParameter otherKeyParameter = null;

	public IESCipher(IESEngine engine)
	{
		this.engine = engine;
	}


	public int engineGetBlockSize() 
	{
		if (engine.getCipher() != null)
		{
			return engine.getCipher().getBlockSize();
		}
		else 
		{
			return 0;
		}
	}


	public int engineGetKeySize(Key key) 
	{
		if (key instanceof ECKey)
		{
			return ((ECKey) key).getParameters().getCurve().getFieldSize();
		}
		else
		{
			throw new IllegalArgumentException("not an EC key");
		}
	}


	public byte[] engineGetIV() 
	{
		return null;
	}


	public AlgorithmParameters engineGetParameters() 
	{
		if (engineParam == null && engineSpec != null)
        {
            try
            {
                    engineParam = AlgorithmParameters.getInstance("IES", BouncyCastleProvider.PROVIDER_NAME);
                    engineParam.init(engineSpec);
            }
            catch (Exception e)
            {
                throw new RuntimeException(e.toString());
            }
        }

		return engineParam;
	}


	public void engineSetMode(String mode) 
			throws NoSuchAlgorithmException 
	{
		String modeName = Strings.toUpperCase(mode);

		if (modeName.equals("NONE"))
		{
            dhaesMode = false;
		}
		else if (modeName.equals("DHAES"))
		{
			dhaesMode = true;
		}
		else
		{
			throw new IllegalArgumentException("can't support mode " + mode);
		}
	}


	public int engineGetOutputSize(int inputLen)    
	{	
		int len1, len2, len3;

		len1 = engine.getMac().getMacSize();

		if (key != null)
		{
			len2 = 1 + 2*(((ECKey) key).getParameters().getCurve().getFieldSize()+7)/8;
		}
		else
		{
			throw new IllegalStateException("cipher not initialised");
		}

		if (engine.getCipher() == null)
		{
			len3 = inputLen;
		}
		else if (state == Cipher.ENCRYPT_MODE || state == Cipher.WRAP_MODE)
		{
			len3 = engine.getCipher().getOutputSize(inputLen);
		}
		else if (state == Cipher.DECRYPT_MODE || state == Cipher.UNWRAP_MODE)
		{
			len3 = engine.getCipher().getOutputSize(inputLen - len1 -len2);	
		}
		else
        {
			throw new IllegalStateException("cipher not initialised");
        }

		if (state == Cipher.ENCRYPT_MODE || state == Cipher.WRAP_MODE)
		{
			return buffer.size() + len1 + len2 + len3;
		}
		else if (state == Cipher.DECRYPT_MODE || state == Cipher.UNWRAP_MODE)
		{
			return buffer.size() - len1 - len2 + len3;
		}
		else
		{
			throw new IllegalStateException("cipher not initialised");
		}

	}

	public void engineSetPadding(String padding)
			throws NoSuchPaddingException
	{
        String paddingName = Strings.toUpperCase(padding);

        // TDOD: make this meaningful...
        if (paddingName.equals("NOPADDING"))
        {

        }
        else if (paddingName.equals("PKCS5PADDING") || paddingName.equals("PKCS7PADDING"))
        {

        }
        else
        {
            throw new NoSuchPaddingException("padding not available with IESCipher");
        }
	}


	// Initialisation methods

	public void engineInit(
			int						opmode,
			Key						key,
			AlgorithmParameters 	params,
			SecureRandom			random) 
					throws InvalidKeyException, InvalidAlgorithmParameterException
	{
		AlgorithmParameterSpec  paramSpec = null;

		if (params != null)     
			try
		{
				paramSpec = params.getParameterSpec(IESParameterSpec.class);
		}
		catch (Exception e)
		{
			throw new InvalidAlgorithmParameterException("cannot recognise parameters: " + e.toString());
		}

		engineParam = params;    
		engineInit(opmode, key, paramSpec, random);

	}


	public void engineInit(
			int						opmode,
			Key						key,
			AlgorithmParameterSpec	engineSpec,
			SecureRandom 			random)
					throws InvalidAlgorithmParameterException, InvalidKeyException
	{
        otherKeyParameter = null;

		// Use default parameters (including cipher key size) if none are specified
        if (engineSpec == null)
        {
            this.engineSpec = IESUtil.guessParameterSpec(engine);
        }
		else if (engineSpec instanceof IESParameterSpec)
		{
			this.engineSpec = (IESParameterSpec)engineSpec;
		}
		else	
		{
			throw new InvalidAlgorithmParameterException("must be passed IES parameters");
		}

		// Parse the recipient's key
		if (opmode == Cipher.ENCRYPT_MODE || opmode == Cipher.WRAP_MODE)
		{
			if (key instanceof ECPublicKey)
			{
				this.key = ECUtil.generatePublicKeyParameter((PublicKey) key);
			}
            else if (key instanceof IESKey)
            {
                IESKey ieKey = (IESKey)key;

                this.key = ECUtil.generatePublicKeyParameter(ieKey.getPublic());
                this.otherKeyParameter = ECUtil.generatePrivateKeyParameter(ieKey.getPrivate());
            }
			else
			{
				throw new InvalidKeyException("must be passed recipient's public EC key for encryption");
			}
		}
		else if (opmode == Cipher.DECRYPT_MODE || opmode == Cipher.UNWRAP_MODE)
		{
			if (key instanceof ECPrivateKey)
			{
				this.key = ECUtil.generatePrivateKeyParameter((PrivateKey)key);
			}
            else if (key instanceof IESKey)
            {
                IESKey ieKey = (IESKey)key;

                this.otherKeyParameter = ECUtil.generatePublicKeyParameter(ieKey.getPublic());
                this.key = ECUtil.generatePrivateKeyParameter(ieKey.getPrivate());
            }
			else
			{
				throw new InvalidKeyException("must be passed recipient's private EC key for decryption");
			}
		}
		else
		{
			throw new InvalidKeyException("must be passed EC key");   
		}


		this.random = random;
		this.state = opmode;
		buffer.reset();

	}


	public void engineInit(
			int                 opmode,
			Key                 key,
			SecureRandom        random) 
			throws InvalidKeyException
	{    
		try
		{
			engineInit(opmode, key, (AlgorithmParameterSpec) null, random);
		}
		catch (InvalidAlgorithmParameterException e)
		{
			throw new IllegalArgumentException("can't handle supplied parameter spec");
		}

	}


	// Update methods - buffer the input
	
	public byte[] engineUpdate(
			byte[]  input,
			int     inputOffset,
			int     inputLen) 
	{
		buffer.write(input, inputOffset, inputLen);
		return null;
	}


	public int engineUpdate(
			byte[]  input,
			int     inputOffset,
			int     inputLen,
			byte[]  output,
			int     outputOffset) 
	{
		buffer.write(input, inputOffset, inputLen);
		return 0;
	}


	// Finalisation methods
	
	public byte[] engineDoFinal(
			byte[]  input,
			int     inputOffset,
			int     inputLen) 
	throws IllegalBlockSizeException, BadPaddingException
	{
		if (inputLen != 0)
		{
			buffer.write(input, inputOffset, inputLen);
		}

		final byte[] in = buffer.toByteArray();
		buffer.reset();

		// Convert parameters for use in IESEngine
		IESParameters params = new IESWithCipherParameters(engineSpec.getDerivationV(),
				engineSpec.getEncodingV(),
				engineSpec.getMacKeySize(),
				engineSpec.getCipherKeySize());

		final ECDomainParameters ecParams = ((ECKeyParameters)key).getParameters();

		final byte[] V;

        if (otherKeyParameter != null)
        {
            try
            {
                if (state == Cipher.ENCRYPT_MODE || state == Cipher.WRAP_MODE)
                {
                    engine.init(true, otherKeyParameter, key, params);
                }
                else
                {
                    engine.init(false, key, otherKeyParameter, params);
                }
                return engine.processBlock(in, 0, in.length);
            }
            catch (Exception e)
            {
                throw new BadPaddingException(e.getMessage());
            }
        }

		if (state == Cipher.ENCRYPT_MODE || state == Cipher.WRAP_MODE)
		{
			// Generate the ephemeral key pair
			ECKeyPairGenerator gen = new ECKeyPairGenerator();
			gen.init(new ECKeyGenerationParameters(ecParams,random));

            EphemeralKeyPairGenerator kGen = new EphemeralKeyPairGenerator(gen, new KeyEncoder()
            {
                public byte[] getEncoded(AsymmetricKeyParameter keyParameter)
                {
                    return ((ECPublicKeyParameters)keyParameter).getQ().getEncoded();
                }
            });

			// Encrypt the buffer
			try
			{
				engine.init(key, params, kGen);

				return engine.processBlock(in, 0, in.length);
			}
			catch (Exception e)
			{
				throw new BadPaddingException(e.getMessage());
			}

		}
		else if (state == Cipher.DECRYPT_MODE || state == Cipher.UNWRAP_MODE)
		{
			// Decrypt the buffer
			try 
			{   			
				engine.init(key, params, new ECIESPublicKeyParser(ecParams));

				return engine.processBlock(in, 0, in.length);
			}
			catch(InvalidCipherTextException e)
			{
				throw new BadPaddingException(e.getMessage());
			}
		}
		else
		{
			throw new IllegalStateException("cipher not initialised");
		}

	}

	public int engineDoFinal(
			byte[] 		input, 
			int 		inputOffset,
			int 		inputLength,
			byte[] 		output,
			int 		outputOffset) 
					throws ShortBufferException, IllegalBlockSizeException, BadPaddingException 
	{

		byte[] buf = engineDoFinal(input, inputOffset, inputLength);
		System.arraycopy(buf, 0, output, outputOffset, buf.length);
		return buf.length;
	}


	/**
	 * Classes that inherit from us
	 */

	static public class ECIES
	extends IESCipher
	{
		public ECIES()
		{
			super(new IESEngine(new ECDHBasicAgreement(),
					new KDF2BytesGenerator(new SHA1Digest()),
					new HMac(new SHA1Digest())));
		}
	}

    static public class ECIESwithDESede
   	extends IESCipher
   	{
   		public ECIESwithDESede()
   		{
   			super(new IESEngine(new ECDHBasicAgreement(),
   					new KDF2BytesGenerator(new SHA1Digest()),
   					new HMac(new SHA1Digest()),
   					new PaddedBufferedBlockCipher(new DESedeEngine())));
   		}
   	}

	static public class ECIESwithAES
	extends IESCipher
	{
		public ECIESwithAES()
		{
			super(new IESEngine(new ECDHBasicAgreement(),
					new KDF2BytesGenerator(new SHA1Digest()),
					new HMac(new SHA1Digest()),
					new PaddedBufferedBlockCipher(new AESEngine())));
		}
	}
}
